<?php

class apnsInit {

    /**
     * Array of APNS Connection Settings
     *
     * @var array
     * @access private
     */
    private $apnsData;

    /**
     * Streams connected to APNS server[s]
     *
     * @var array
     * @access private
     */
    private $sslStreams;

    /**
     * Absolute path to your Production Certificate
     *
     * @var string
     * @access private
     */
    private $certificate;
    private $passphrase;

    /**
     * Apples Production APNS Gateway
     *
     * @var string
     * @access private
     */
    private $ssl = 'ssl://gateway.push.apple.com:2195';

    /**
     * Apples Production APNS Feedback Service
     *
     * @var string
     * @access private
     */
    private $feedback = 'ssl://feedback.push.apple.com:2196';

    /**
     * Absolute path to your Development Certificate
     *
     * @var string
     * @access private
     */
    private $sandboxCertificate;

    /**
     * Apples Sandbox APNS Gateway
     *
     * @var string
     * @access private
     */
    private $sandboxSsl = 'ssl://gateway.sandbox.push.apple.com:2195';

    /**
     * Apples Sandbox APNS Feedback Service
     *
     * @var string
     * @access private
     */
    private $sandboxFeedback = 'ssl://feedback.sandbox.push.apple.com:2196';
    private static $statusDesc = array(
        0 => 'No errors encountered',
        1 => 'Processing error',
        2 => 'Missing device token',
        3 => 'Missing topic',
        4 => 'Missing payload',
        5 => 'Invalid token size',
        6 => 'Invalid topic size',
        7 => 'Invalid payload size',
        8 => 'Invalid token',
        255 => 'None (unknown)'
    );

    /*
     * public function __construct() {
     *
     * }
     */

    public function loadcertificate($config) {
        $this->certificate = $config['certificate']; // CERTIFICATE;
        $this->sandboxCertificate = $config['sandboxcertificate']; // SANDBOXCERTIFICATE;
        $this->passphrase = $config['apns_pass_phrase']; // APNS_PASS_PHRASE;
        $this->sanbox_passphrase = $config['sandbox_apns_pass_phrase']; // APNS_PASS_PHRASE;
        $this->apnsData = array(
            'production' => array(
                'certificate' => $this->certificate,
                'ssl' => $this->ssl,
                'feedback' => $this->feedback,
                'passphrase' => $this->passphrase
            ),
            'sandbox' => array(
                'certificate' => $this->sandboxCertificate,
                'ssl' => $this->sandboxSsl,
                'feedback' => $this->sandboxFeedback,
                'passphrase' => $this->sanbox_passphrase
            )
        );
    }

    public function push($message, $token, $development = null, $pid = '100') {
        if (!$development) {
            $development = 'production';
        }
        $json_encoded_message = json_encode(self::urlDecodeOnMap($message));
        $json_encoded_message = str_replace(array(
            "\r\n",
            "\r",
            "\n"
                ), "", $json_encoded_message);
        $fp = false;
        if (isset($this->sslStreams[$development])) {
            $fp = $this->sslStreams[$development];
        } else {
            $fp = $this->_connectSSLSocket($development);
        }
        if (!$fp) {
            writeLog(date("Y-m-d H:i:s") . "||A connected socket to APNS wasn't available\r\n", 'apns_error');
            return false;
        }
        $expiry = time() + 604800; // 1周超时
        $msg = chr(1) . pack("N", $pid) . pack("N", $expiry) . pack("n", 32) . pack('H*', $token) . pack("n", strlen($json_encoded_message)) . $json_encoded_message;
        $fwrite = @fwrite($fp, $msg);
        if (!$fwrite) {
            writeLog(date("Y-m-d H:i:s") . "||Failed writing to stream.\r\n", 'apns_error');
            $this->_closeSSLSocket($development);
            // try again
            $fp = $this->_connectSSLSocket($development);
            if (!$fp) {
                writeLog(date("Y-m-d H:i:s") . "||A connected socket to APNS wasn't available ||in retry\r\n", 'apns_error');
                return false;
            }
            $fwrite = fwrite($fp, $msg);
            if (!$fwrite) {
                writeLog(date("Y-m-d H:i:s") . "||Failed writing to stream.||in retry\r\n", 'apns_error');
                $fp = $this->_connectSSLSocket($development);
                return false;
            }
        }
        $tv_sec = 0;
        $tv_usec = 1500000; // Timeout. 1 million micro seconds = 1 second
        $r = array(
            $fp
        );
        $we = null; // Temporaries. "Only variables can be passed as reference."
        $numChanged = stream_select($r, $we, $we, $tv_sec, $tv_usec);
        // $numChanged=0;
        if (false === $numChanged) {
            writeLog(date("Y-m-d H:i:s") . "||Failed selecting stream to read.\r\n", 'apns_error');
        } else
        if ($numChanged > 0) {
            $command = ord(fread($fp, 1));
            $status = ord(fread($fp, 1));
            $identifier_string = fread($fp, 4);
            // 经过多次测试，有时候通道出现异常时，读到的$numChanged=1，但是读取到的数据都是空的，只有当$command=8时才为苹果错误结果的正常返回。
            if ($command == 8) {
                if ($status > 0) {
                    $identifier = implode('', unpack("N", $identifier_string));
                    $desc = isset(self::$statusDesc[$status]) ? self::$statusDesc[$status] : 'Unknown';
                    writeLog(date("Y-m-d H:i:s") . "||APNS responded with error for command($command)  pid($identifier) status($status: $desc) token($token). \r\n", 'apns_error');
                    // The socket has also been closed. Cause reopening in the loop outside.
                    $this->_closeSSLSocket($development);
                    sleep(1);
                    return false;
                }
                writeLog(date("Y-m-d H:i:s") . "发送成功 token（$token）", "apns_suc");
                return true;
            } else {
                writeLog(date("Y-m-d H:i:s") . "||APNS responded with exception stream token($token).\r\n", 'apns_error');
                $this->_closeSSLSocket($development);
                sleep(1);
                return false;
            }
        }
        writeLog(date("Y-m-d H:i:s") . "||发送状态未知：numChanged: $numChanged token($token). \r\n", 'apns_time_out');
        return true;
    }

    public function push_debug($message, $token, $development = null, $pid = '100') {
        if (!$development) {
            $development = 'production';
        }
        $json_encoded_message = json_encode(self::urlDecodeOnMap($message));
        writeLog($json_encoded_message . "\r\n", "message");
        $fp = false;
        if (isset($this->sslStreams[$development])) {
            $fp = $this->sslStreams[$development];
        } else {
            $fp = $this->_connectSSLSocket($development);
        }
        if (!$fp) {
            $this->_closeSSLSocket($development);
            return date("Y-m-d H:i:s") . "||一个连接到APN的socket不可用";
        }
        $expiry = time() + 604800; // 1周超时
        $msg = chr(1) . pack("N", $pid) . pack("N", $expiry) . pack("n", 32) . pack('H*', $token) . pack("n", strlen($json_encoded_message)) . $json_encoded_message;
        $fwrite = @fwrite($fp, $msg);
        if (!$fwrite) {
            $this->_closeSSLSocket($development);
            // try again
            $fp = $this->_connectSSLSocket($development);
            if (!$fp) {
                $this->_closeSSLSocket($development);
                return date("Y-m-d H:i:s") . "||一个重试连接到APN的socket不可用";
            }
            $fwrite = fwrite($fp, $msg);
            if (!$fwrite) {
                $fp = $this->_connectSSLSocket($development);
                $this->_closeSSLSocket($development);
                return date("Y-m-d H:i:s") . "||重试过程中仍未能写入流";
            }
        }
        $tv_sec = 1;
        $tv_usec = null; // Timeout. 1 million micro seconds = 1 second
        $r = array(
            $fp
        );
        $we = null; // Temporaries. "Only variables can be passed as reference."
        $numChanged = stream_select($r, $we, $we, $tv_sec, $tv_usec);
        if (false === $numChanged) {
            return date("Y-m-d H:i:s") . "||选择流读取失败";
        } else
        if ($numChanged > 0) {
            $command = ord(fread($fp, 1));
            $status = ord(fread($fp, 1));
            $identifier_string = fread($fp, 4);
            if ($identifier_string) {
                $identifier = implode('', unpack("N", $identifier_string));
                $statusDesc = array(
                    0 => '正确',
                    1 => '处理过程错误',
                    2 => '缺少设备令牌',
                    3 => '缺少主题',
                    4 => '缺少有效消息体',
                    5 => '令牌长度错误',
                    6 => '大小错误',
                    7 => '消息体长度错误',
                    8 => '令牌错误',
                    255 => '未知错误'
                );
                if ($status > 0) {
                    $desc = isset($statusDesc[$status]) ? $statusDesc[$status] : 'Unknown';

                    // The socket has also been closed. Cause reopening in the loop outside.
                    $this->_closeSSLSocket($development);
                    return "||错误原因：" . $desc;
                } else {
                    $this->_closeSSLSocket($development);
                    return "发送成功！command($command) status($status) pid($identifier)";
                }
            }
        }
        $this->_closeSSLSocket($development);
        return "||Numchanged:" . $numChanged . "||token:" . $token;
    }

    /**
     * Connect the SSL stream (sandbox or production)
     *
     * @param $development string
     *            Development environment - sandbox or production
     * @return bool|resource status whether the socket connected or not.
     * @access private
     */
    private function _connectSSLSocket($development) {
        $ctx = stream_context_create();
        stream_context_set_option($ctx, 'ssl', 'local_cert', $this->apnsData[$development]['certificate']);
        if ($this->apnsData[$development]['passphrase']) {
            stream_context_set_option($ctx, 'ssl', 'passphrase', $this->apnsData[$development]['passphrase']);
        }
        $this->sslStreams[$development] = @stream_socket_client($this->apnsData[$development]['ssl'], $error, $errorString, 100, (STREAM_CLIENT_CONNECT | STREAM_CLIENT_PERSISTENT), $ctx);
        if (!$this->sslStreams[$development]) {
            writeLog("Failed to connect to APNS: {$error} {$errorString}.\r\n", 'apns_error');
            unset($this->sslStreams[$development]);
            return false;
        }
        return $this->sslStreams[$development];
    }

    /**
     * Close the SSL stream (sandbox or production)
     *
     * @param $development string
     *            Development environment - sandbox or production
     * @return void
     * @access private
     */
    public function _closeSSLSocket($development) {
        if (isset($this->sslStreams[$development])) {
            fclose($this->sslStreams[$development]);
            unset($this->sslStreams[$development]);
        }
    }

    /**
     * Fetch APNS Messages
     *
     * This will check with APNS for any invalid tokens and disable them from receiving further notifications.
     *
     * @param string $development
     *            Which SSL to connect to, Sandbox or Production
     * @access private
     */
    public function checkFeedback($development = null) {
        if (!$development) {
            $development = 'production';
        }
        $ctx = stream_context_create();
        stream_context_set_option($ctx, 'ssl', 'local_cert', $this->apnsData[$development]['certificate']);
        stream_context_set_option($ctx, 'ssl', 'verify_peer', false);
        if ($this->apnsData[$development]['passphrase']) {
            stream_context_set_option($ctx, 'ssl', 'passphrase', $this->apnsData[$development]['passphrase']);
        }
        $fp = @stream_socket_client($this->apnsData[$development]['feedback'], $error, $errorString, 100, (STREAM_CLIENT_CONNECT | STREAM_CLIENT_PERSISTENT), $ctx);

        if (!$fp) {
            writeLog("Failed to connect to feedback service: {$error} {$errorString}.\r\n", 'apns_feedback');
            return null;
        }
        $tokens = array();
        while ($devcon = fread($fp, 38)) {
            $arr = unpack("H*", $devcon);
            $rawhex = trim(implode("", $arr));
            $token = substr($rawhex, 12, 64);
            if (!empty($token)) {
                $tokens[] = $token;
            }
        }
        fclose($fp);
        return $tokens;
    }

    /**
     * 数组循环urldecode
     */
    protected static function urlDecodeOnMap($data) {
        return is_array($data) ? array_map(array('apnsInit', 'urlDecodeOnMap'), $data) : (is_numeric($data) ? $data : urldecode($data) );
    }

}

?>
